/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "js_selection_ability.h"

#include "event_checker.h"
#include "selection_js_utils.h"
#include "napi/native_node_api.h"
#include "selection_log.h"
#include "util.h"

namespace OHOS {
namespace SelectionFwk {
constexpr size_t ARGC_ONE = 1;
constexpr size_t ARGC_TWO = 2;

const std::string JsSelectionAbility::KDS_CLASS_NAME = "SelectionAbility";
thread_local napi_ref JsSelectionAbility::KDSRef_ = nullptr;
std::mutex JsSelectionAbility::selectionMutex_;
std::shared_ptr<JsSelectionAbility> JsSelectionAbility::selectionDelegate_{ nullptr };

napi_value JsSelectionAbility::GetSelectionAbility(napi_env env, napi_callback_info info)
{
    SELECTION_HILOGI("GetSelectionAbility");
    return nullptr;
}

std::shared_ptr<JsSelectionAbility> JsSelectionAbility::GetJsSelectionAbility()
{
    if (selectionDelegate_ == nullptr) {
        std::lock_guard<std::mutex> lock(selectionMutex_);
        if (selectionDelegate_ == nullptr) {
            auto delegate = std::make_shared<JsSelectionAbility>();
            if (delegate == nullptr) {
                SELECTION_HILOGE("keyboard delegate is nullptr!");
                return nullptr;
            }
            selectionDelegate_ = delegate;
        }
    }
    return selectionDelegate_;
}

napi_value JsSelectionAbility::JsConstructor(napi_env env, napi_callback_info cbinfo)
{
    napi_value thisVar = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, cbinfo, nullptr, nullptr, &thisVar, nullptr));
    auto delegate = GetJsSelectionAbility();
    if (delegate == nullptr) {
        SELECTION_HILOGE("failed to get delegate!");
        napi_value result = nullptr;
        napi_get_null(env, &result);
        return result;
    }
    napi_status status = napi_wrap(
        env, thisVar, delegate.get(), [](napi_env env, void *nativeObject, void *hint) {}, nullptr, nullptr);
    if (status != napi_ok) {
        SELECTION_HILOGE("failed to wrap: %{public}d!", status);
        return nullptr;
    }
    return thisVar;
};

napi_value JsSelectionAbility::Subscribe(napi_env env, napi_callback_info info)
{
    size_t argc = ARGC_TWO;
    napi_value argv[ARGC_TWO] = { nullptr };
    napi_value thisVar = nullptr;
    void *data = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, &data));
    std::string type;
    // 2 means least param num.
    if (argc < 2 || !JsUtil::GetValue(env, argv[0], type) ||
        !EventChecker::IsValidEventType(EventSubscribeModule::SELECTION_METHOD_ABILITY, type) ||
        JsUtil::GetType(env, argv[1]) != napi_function) {
        SELECTION_HILOGE("subscribe failed, type: %{public}s!", type.c_str());
        return nullptr;
    }
    SELECTION_HILOGE("subscribe type: %{public}s.", type.c_str());
    auto selectionAbility = reinterpret_cast<JsSelectionAbility *>(JsUtils::GetNativeSelf(env, info));
    if (selectionAbility == nullptr) {
        return nullptr;
    }
    std::shared_ptr<JSCallbackObject> callback =
        std::make_shared<JSCallbackObject>(env, argv[1], std::this_thread::get_id(),
            AppExecFwk::EventHandler::Current());
    selectionAbility->RegisterListener(argv[ARGC_ONE], type, callback);

    napi_value result = nullptr;
    napi_get_null(env, &result);
    return result;
}


napi_value JsSelectionAbility::UnSubscribe(napi_env env, napi_callback_info info)
{
    size_t argc = ARGC_TWO;
    napi_value argv[ARGC_TWO] = { nullptr };
    napi_value thisVar = nullptr;
    void *data = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, &data));
    std::string type;
    // 1 means least param num.
    if (argc < 1 || !JsUtil::GetValue(env, argv[0], type) ||
        !EventChecker::IsValidEventType(EventSubscribeModule::SELECTION_METHOD_ABILITY, type)) {
        SELECTION_HILOGE("unsubscribe failed, type: %{public}s!", type.c_str());
        return nullptr;
    }

    // if the second param is not napi_function/napi_null/napi_undefined, return
    auto paramType = JsUtil::GetType(env, argv[1]);
    if (paramType != napi_function && paramType != napi_null && paramType != napi_undefined) {
        return nullptr;
    }
    // if the second param is napi_function, delete it, else delete all
    argv[1] = paramType == napi_function ? argv[1] : nullptr;

    SELECTION_HILOGD("unsubscribe type: %{public}s.", type.c_str());
    auto delegate = reinterpret_cast<JsSelectionAbility *>(JsUtils::GetNativeSelf(env, info));
    if (delegate == nullptr) {
        return nullptr;
    }
    delegate->UnRegisterListener(argv[ARGC_ONE], type);
    napi_value result = nullptr;
    napi_get_null(env, &result);
    return result;
}

void JsSelectionAbility::RegisterListener(napi_value callback, std::string type,
    std::shared_ptr<JSCallbackObject> callbackObj)
{
    SELECTION_HILOGD("RegisterListener %{public}s", type.c_str());
    std::lock_guard<std::recursive_mutex> lock(mutex_);
    if (jsCbMap_.empty() || jsCbMap_.find(type) == jsCbMap_.end()) {
        SELECTION_HILOGD("methodName %{public}s is not registered!", type.c_str());
    }
    auto callbacks = jsCbMap_[type];
    bool ret = std::any_of(callbacks.begin(), callbacks.end(), [&callback](std::shared_ptr<JSCallbackObject> cb) {
        return JsUtils::Equals(cb->env_, callback, cb->callback_, cb->threadId_);
    });
    if (ret) {
        SELECTION_HILOGD("JsSelectionAbility callback already registered!");
        return;
    }

    SELECTION_HILOGI("add %{public}s callbackObj into jsCbMap_.", type.c_str());
    jsCbMap_[type].push_back(std::move(callbackObj));
}

void JsSelectionAbility::UnRegisterListener(napi_value callback, std::string type)
{
    SELECTION_HILOGI("unregister listener: %{public}s.", type.c_str());
    std::lock_guard<std::recursive_mutex> lock(mutex_);
    if (jsCbMap_.empty() || jsCbMap_.find(type) == jsCbMap_.end()) {
        SELECTION_HILOGE("methodName %{public}s is not unregistered!", type.c_str());
        return;
    }

    if (callback == nullptr) {
        jsCbMap_.erase(type);
        SELECTION_HILOGE("callback is nullptr!");
        return;
    }

    for (auto item = jsCbMap_[type].begin(); item != jsCbMap_[type].end(); item++) {
        if ((callback != nullptr) &&
            (JsUtils::Equals((*item)->env_, callback, (*item)->callback_, (*item)->threadId_))) {
            jsCbMap_[type].erase(item);
            break;
        }
    }
    if (jsCbMap_[type].empty()) {
        jsCbMap_.erase(type);
    }
}

napi_value JsSelectionAbility::Init(napi_env env, napi_value exports)
{
    SELECTION_HILOGI("napi init");
    napi_property_descriptor properties[] = {
        DECLARE_NAPI_FUNCTION("getSelectionAbility", GetSelectionAbility),
        DECLARE_NAPI_FUNCTION("on", Subscribe),
        DECLARE_NAPI_FUNCTION("off", UnSubscribe),
    };

    napi_value cons = nullptr;
    NAPI_CALL(env, napi_define_class(env, KDS_CLASS_NAME.c_str(), KDS_CLASS_NAME.size(), JsConstructor, nullptr,
                       sizeof(properties) / sizeof(napi_property_descriptor), properties, &cons));
    NAPI_CALL(env, napi_create_reference(env, cons, 1, &KDSRef_));
    NAPI_CALL(env, napi_set_named_property(env, exports, KDS_CLASS_NAME.c_str(), cons));
    return exports;
}
} // namespace OHOS::SelectionFwk
} // namespace OHOS
